// 
// Copyright 2010 Classpath Pty Ltd <opensource@classpath.com.au>
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 

using sql

**
** The `Manager` provides the core engine for running a set of migrations and database preparation against a given `sql::SqlService` with a supplied `Adapter`.
**
internal class Manager {

    **
    ** The `Adapter` to use in generating SQL to be executed.
    **
    Adapter adapter

    **
    ** The [Migrations]`Migration` registered for this `Manager` to run.
    **
    Migration[] migrations
  
    **
    ** Stores the JDBC uri to connect to the database being migrated with.
    ** 
    Str uri
  
    **
    ** Stores the JDBC username to connect to the database being migrated with.
    ** 
    Str? username
  
    **
    ** Stores the JDBC password to connect to the database being migrated with.
    ** 
    Str? password
    **
    ** Whether to prompt the user for confirmation before running each command.
    **
    Bool interactive

    **
    ** Creates a new `Manager`.
    **
    new make(Migration[] migrations, Adapter adapter, Str uri, Str? username := null, Str? password := null, Bool interactive := false) {
        Pod.of(this).log.debug("${typeof}.make(${migrations}, ${adapter}, ${uri}, ${username}, ${password}, ${interactive}) - start")
        this.migrations = migrations
        this.adapter = adapter
        this.uri = uri
        this.username = username
        this.password = password
        this.interactive = interactive
        Pod.of(this).log.debug("${typeof}.make(${migrations}, ${adapter}, ${uri}, ${username}, ${password}, ${interactive}) - end")
    }

    **
    ** Prepares the database for migrations by running `MigrationZero.up`.
    **
    Bool prepare() {
        Pod.of(this).log.debug("${typeof}.prepare - start")
        aborted := false
        sqlConn := SqlConn.open(uri, username, password)
        try {
            sqlConn.autoCommit = false
            if (sqlConn.meta.tableExists(MigrationZero.tableName)) {
                Pod.of(this).log.info("Version history table '${MigrationZero.tableName}' already exists - no need to init")
            }
            else {
                Pod.of(this).log.info("Version history table '${MigrationZero.tableName}' doesn't exist - creating it (SQL follows)")
                migrationZero := MigrationZero()
                migrationZero.reset
                migrationZero.up
                migrationZero.modifications.each |Modification modification| {
                    if (!aborted) {
                        sql := adapter.sql(modification)
                        Pod.of(this).log.info(sql.splitLines.join(" "))
                        if (interactive) {
                            echo("".padr(120, '-'))
                            echo(sql)
                            echo("".padr(120, '-'))
                            echo("Continue? (Y/n)")
                            echo("".padr(120, '-'))
                            aborted = Env.cur.in.readLine.equalsIgnoreCase("n")
                        }
                        if (!aborted) {
                            sqlConn.sql(sql).execute
                            sqlConn.commit
                        }
                        else {
                            Pod.of(this).log.warn("Aborting due to user input - you may need to clean up the database yourself")
                            aborted = true
                        }
                    }
                }
                if (!aborted) {
                    logMigration(migrationZero.from, migrationZero.to)
                }
            }
        }
        catch {
            sqlConn.rollback
        }
        finally {
            sqlConn.close
        }
        Pod.of(this).log.debug("${typeof}.prepare - end - returning ${!aborted}")
        return !aborted
    }

    **
    ** Returns the `sys::Version` of the database, or 'null' if the database has not been [prepared]`Manager.prepare`.
    **
    Version? dbVersion() {
        Pod.of(this).log.debug("${typeof}.dbVersion - start")
        Version? version
        sqlConn := SqlConn.open(uri, username, password)
        try {
            if (!sqlConn.meta.tableExists(MigrationZero.tableName)) {
                Pod.of(this).log.info("Database doesn't containt ${MigrationZero.tableName} (have you prepared it?)")
            }
            else {
                Pod.of(this).log.info("Getting current version of database (SQL follows)")
                sql := adapter.dbVersionSql
                Pod.of(this).log.info(sql.splitLines.join(" "))
                sqlConn.sql(sql).queryEach([:]) | Row row | {
                    col := row.col(MigrationZero.toVersionColumnName)
                    version = Version(row.get(col).toStr)
                }
            }
        }
        finally {
            sqlConn.close
        }
        Pod.of(this).log.debug("${typeof}.dbVersion - returning ${version}")
        return version
    }

    **
    ** Stores the migration between supplied [Versions]`sys::Version` in the database.
    **
    Void logMigration(Version from, Version to) {
        Pod.of(this).log.debug("${typeof}.logMigration(${from}, ${to}) - start")
        sqlConn := SqlConn.open(uri, username, password)
        try {
            sqlConn.autoCommit = false
            Pod.of(this).log.info("Logging migration of database from ${from} to ${to} (SQL follows)")
            sql := adapter.logMigrationSql
            Pod.of(this).log.info(sql.splitLines.join(" "))
            sqlConn.sql(sql).prepare.execute(["from": from.toStr, "to": to.toStr])
            sqlConn.commit
        }
        catch {
            sqlConn.rollback
        }
        finally {
            sqlConn.close
        }
        Pod.of(this).log.debug("${typeof}.logMigration(${from}, ${to}) - end")
    }

    **
    ** Attempts to migrate the database between the specified versions.
    **
    Bool migrate(Version from, Version to) {
        Pod.of(this).log.debug("${typeof}.migrate(${from}, ${to}) - start")
        router := Router(migrations)
        steps := router.route(from, to)
        aborted := false
        steps.each |Migration migration| {
            if (!aborted) {
                version := dbVersion
                if (version == null) {
                    Pod.of(this).log.err("Cannot perform a migration on a database that hasn't been initialised (try '-p')")
                    throw Err("Cannot perform a migration on a database that hasn't been initialised (try '-i')")
                }
                migration.reset
                if (from < to) {
                    if (version != migration.from) {
                        Pod.of(this).log.err("Cannot perform a migration up from version ${migration.from} when the database is at version ${version}")
                        throw Err("Cannot perform a migration up from version ${migration.from} when the database is at version ${version}")
                    }
                    Pod.of(this).log.info("Migrating database from ${migration.from} to ${migration.to} (SQL follows)")
                    migration.up
                }
                else {
                    if (version != migration.to) {
                        Pod.of(this).log.err("Cannot perform a migration down from version ${migration.to} when the database is at version ${version}")
                        throw Err("Cannot perform a migration down from version ${migration.to} when the database is at version ${version}")
                    }
                    Pod.of(this).log.info("Migrating database from ${migration.to} to ${migration.from} (SQL follows)")
                    migration.down
                }
                migration.modifications.each |Modification modification| {
                    if (!aborted) {
                        sqlConn := SqlConn.open(uri, username, password)
                        try {
                            sqlConn.autoCommit = false
                            sql := adapter.sql(modification)
                            Pod.of(this).log.info(sql.splitLines.join(" "))
                            if (interactive) {
                                echo("".padr(120, '-'))
                                echo(sql)
                                echo("".padr(120, '-'))
                                echo("Continue? (Y/n)")
                                echo("".padr(120, '-'))
                                aborted = Env.cur.in.readLine.equalsIgnoreCase("n")
                            }
                            if (!aborted) {
                                sqlConn.sql(sql).execute
                                sqlConn.commit
                            }
                            else {
                                Pod.of(this).log.warn("Aborting due to user input - you may need to clean up the database yourself")
                                aborted = true
                            }
                        }
                        catch {
                            sqlConn.rollback
                        }
                        finally {
                            sqlConn.close
                        }
                    }
                }
                if (!aborted) {
                    if (from < to) {
                        logMigration(migration.from, migration.to)
                    }
                    else {
                        logMigration(migration.to, migration.from)
                    }
                }
            }
        }
        Pod.of(this).log.debug("${typeof}.migrate(${from}, ${to}) - end - returning ${!aborted}")
        return !aborted
    }

}
